/**
 * 代码缺陷模型
 *
 * Created by yangyunze on 2021/05/31.
 */
const mongoose = require('mongoose');
const randomstring = require('randomstring');
const fs = require('fs-extra');
const path = require('path');
const publisher = require('./redis/publisher');
const testcases = require('./tests');
const File = require('./mongo/file');
const filesModels = require('../models/files');
const Excel = require('./export/report/adapter/excel');
const fileElementsModels = require('../models/file-elements');
const Function = require('./mongo/function');
const CodeDefect = require('./mongo/code-defect');
const utility = require('./utility');
const cacheUtil = require('../cacheUtil');
const { eventHub, eventKind } = require('./event-hub');

const defectKind = {
  divideZero: 1,
  nullPointer: 2,
  outOfBounds: 3,
  deadEnd: 4,
};

const defectKindDesc = {
  [defectKind.divideZero]: '除零',
  [defectKind.nullPointer]: '空指针',
  [defectKind.outOfBounds]: '数组越界',
  [defectKind.deadEnd]: '不可达代码',
};

const locationType = {
  single: 'single',
  range: 'range',
};

function getJson(defect) {
  defect = defect.toObject();
  defect.defectId = defect._id.toString();
  delete defect._id;
  delete defect.__v;
  return defect;
}

function prepareTestcases(testIds) {
  if (utility.isEmpty(testIds)) {
    return Promise.resolve([]);
  }
  if (!utility.isArrayType(testIds)) {
    testIds = [testIds];
  }
  return testcases.getTests({ testIds })
    .then((testcases) => {
      const newResult = [];
      const newTestCaseIds = testIds.map((id) => id.toString());
      for (let i = 0; i < testcases.length; i += 1) {
        const newObj = {};
        const testcase = testcases[i];
        const index = newTestCaseIds.indexOf(testcase._id.toString());
        if (index !== -1) {
          newObj.id = testcase._id.toString();
          newObj.number = testcase.number;
          newResult[index] = newObj;
        }
      }
      return newResult;
    });
}

function getCodeDefectsGroup(
  groupFields = [],
  { versionId = null, fileIds = [], functionIds = [] }
) {
  if (!utility.isArrayType(groupFields)) {
    groupFields = [groupFields];
  }
  if (!utility.isArrayType(fileIds)) {
    fileIds = [fileIds];
  }
  if (!utility.isArrayType(functionIds)) {
    functionIds = [functionIds];
  }
  if (groupFields.length === 0) {
    return Promise.resolve({});
  }
  const options = {};
  if (fileIds.length > 0) {
    options.fileId = { $in: fileIds.map((id) => mongoose.Types.ObjectId(id)) };
  }
  if (functionIds.length > 0) {
    options.functionId = { $in: functionIds.map((id) => mongoose.Types.ObjectId(id)) };
  }
  if (versionId) {
    options.versionId = mongoose.Types.ObjectId(versionId);
  }
  const group = {};
  group._id = groupFields.reduce((pre, field) => {
    pre[field] = `$${field}`;
    return pre;
  }, {});
  return CodeDefect.aggregate([
    { $match: options },
    { $group: group },
  ])
    .then((results) => {
      const groupInfo = {};
      results.forEach((result) => {
        groupInfo[Object.values(result._id).join('|')] = 1;
      });
      return groupInfo;
    });
}

function getUnreadTotal(functionId) {
  return CodeDefect.countDocuments(
    { functionId: mongoose.Types.ObjectId(functionId), unread: true }
  );
}

function getCodeDefect(defectId) {
  return CodeDefect.findById(mongoose.Types.ObjectId(defectId));
}

function getCodeDefects(functionId, query) {
  const options = { ...query };
  if (functionId) {
    options.functionId = functionId;
  }
  if (query.manual) {
    options.manual = !!Number(query.manual);
  }
  return CodeDefect.find(options).sort({ _id: 1 });
}

function getCodeDefectsByFildId(fileId) {
  return CodeDefect.find({
    fileId: mongoose.Types.ObjectId(fileId),
  }).sort({ _id: 1 });
}

function createCodeDefect(projectId, versionId, fileId, functionId,
  {
    description = '',
    manual = true,
    locations = [],
    testIds = [],
    type = undefined,
  }) {
  if (!utility.isArrayType(locations)) {
    locations = [locations];
  }
  locations.map((location) => {
    if (!location.type) {
      location.type = locationType.single;
    }
    return location;
  });
  return prepareTestcases(testIds)
    .then((testcases) => {
      const defect = new CodeDefect({
        projectId: mongoose.Types.ObjectId(projectId),
        versionId: mongoose.Types.ObjectId(versionId),
        fileId: mongoose.Types.ObjectId(fileId),
        functionId: mongoose.Types.ObjectId(functionId),
        description,
        manual,
        locations,
        type,
        testcases,
      });
      return defect.save();
    })
    .then((defect) => {
      publisher.publishCodeDefectMessage('add', getJson(defect));
      getUnreadTotal(functionId)
        .then((unreadTotal) => {
          publisher.publishCodeDefectMessage('update', {
            functionId,
            versionId,
            unreadTotal,
          });
        });
      return defect;
    });
}

function deleteCodeDefects(options) {
  let defectsInfo = [];
  return CodeDefect.find(options, ['unread', 'functionId', 'versionId', 'testcases'])
    .then((defects) => {
      defectsInfo = defects;
      return CodeDefect.deleteMany({ _id: { $in: defects.map((defect) => defect._id) } });
    })
    .then(() => {
      defectsInfo.forEach((defect) => {
        defect = getJson(defect);
        defect.deletedTestIds = defect.testcases.map((testcase) => testcase.id);
        publisher.publishCodeDefectMessage('delete', defect);
      });
      // publish unread count message
      if (defectsInfo.filter((defect) => defect.unread).length > 0) {
        const { functionId, versionId } = defectsInfo[0];
        getUnreadTotal(functionId)
          .then((unreadTotal) => {
            publisher.publishCodeDefectMessage('update', {
              functionId,
              versionId,
              unreadTotal,
            });
          });
      }
    });
}

function deleteCodeDefect(defectId) {
  return deleteCodeDefects({ _id: mongoose.Types.ObjectId(defectId) });
}

function updateCodeDefects(options, doc) {
  return CodeDefect.find(options, ['testcases'])
    .then((defects) => Promise.all(
      defects.map((orgDefect) => CodeDefect.findOneAndUpdate(
        { _id: orgDefect._id },
        doc,
        { new: true },
      )
        .then((defect) => {
          defect = getJson(defect);
          defect.deletedTestIds = orgDefect.testcases.reduce((ids, orgTest) => {
            let deleted = true;
            defect.testcases.forEach((test) => {
              if (test.id === orgTest.id) {
                deleted = false;
              }
            });
            if (deleted) {
              ids.push(orgTest.id);
            }
            return ids;
          }, []);
          publisher.publishCodeDefectMessage('update', defect);
          return defect;
        }))
    ))
    .then((defects) => {
      // publish unread count message
      if (doc.$set && doc.$set.unread !== undefined && defects.length > 0) {
        const { functionId, versionId } = defects[0];
        getUnreadTotal(functionId)
          .then((unreadTotal) => {
            publisher.publishCodeDefectMessage('update', {
              functionId,
              versionId,
              unreadTotal,
            });
          });
      }
      return defects;
    });
}

function updateCodeDefect(defectId, body) {
  const doc = { unread: true };
  let preOperation = Promise.resolve(null);
  if (body.testIds !== undefined) {
    preOperation = prepareTestcases(body.testIds);
  }
  if (body.manual !== undefined) {
    doc.manual = !!Number(body.manual);
  }
  if (body.unread !== undefined) {
    doc.unread = !!Number(body.unread);
  }
  if (body.description !== undefined) {
    doc.description = body.description;
  }
  if (body.locations !== undefined) {
    doc.locations = body.locations;
  } else {
    doc.locations = [];
  }
  return preOperation
    .then((testcases) => {
      if (testcases !== null) {
        doc.testcases = testcases;
      }
      return updateCodeDefects(
        { _id: mongoose.Types.ObjectId(defectId) },
        { $set: doc }
      );
    })
    .then(([defect]) => defect);
}

function setCodeDefectToManual({ functionId, testIds = [] }) {
  const options = { manual: false };
  if (functionId) {
    options.functionId = mongoose.Types.ObjectId(functionId);
  }
  if (testIds.length > 0) {
    options.testcases = {
      $elemMatch: { id: { $in: testIds.map((id) => mongoose.Types.ObjectId(id)) } },
    };
  }
  return updateCodeDefects(options, { $set: { manual: true } });
}

function processAutoDefects(testKind, locations, {
  projectId,
  versionId,
  fileId,
  functionId,
  testId,
}) {
  let type = '';
  // eslint-disable-next-line no-bitwise
  if ((testKind & testcases.testKind.divideZero) > 0) {
    type = defectKind.divideZero;
    // eslint-disable-next-line no-bitwise
  } else if ((testKind & testcases.testKind.nullPointer) > 0) {
    type = defectKind.nullPointer;
    // eslint-disable-next-line no-bitwise
  } else if ((testKind & testcases.testKind.outOfBounds) > 0) {
    type = defectKind.outOfBounds;
  } else {
    return Promise.resolve();
  }
  const data = {
    manual: false,
    locations,
    type,
    testIds: [testId],
  };
  return deleteCodeDefects({
    functionId: mongoose.Types.ObjectId(functionId),
    type,
    manual: false,
    testcases: { $elemMatch: { id: testId.toString() } },
  })
    .then(() => createCodeDefect(projectId, versionId, fileId, functionId, data));
}

function checkDeadEnd(cfg, extraInfo) {
  const { nodes, edges } = cfg;
  const relation = {};
  let startNodeId;
  // format the edges
  Object.keys(edges).forEach((key) => {
    if (!relation[edges[key].startNodeId]) {
      relation[edges[key].startNodeId] = [];
    }
    relation[edges[key].startNodeId].push(edges[key].endNodeId);
  });
  // get the start node
  Object.keys(nodes).forEach((key) => {
    if (nodes[key].kind === 1) {
      startNodeId = key;
    }
  });
  const arr = new Set();
  if (startNodeId) {
    arr.add(startNodeId);
  }
  while (arr.size > 0) {
    const nodeId = arr.values().next().value;
    arr.delete(nodeId);
    delete nodes[nodeId];
    if (relation[nodeId]) {
      relation[nodeId].filter((nodeId) => nodes[nodeId]).forEach((nodeId) => arr.add(nodeId));
    }
  }
  const { functionId } = extraInfo;
  return deleteCodeDefects(
    {
      functionId: mongoose.Types.ObjectId(functionId),
      type: defectKind.deadEnd,
      manual: false,
    }
  ).then(() => {
    const promises = [];
    if (Object.keys(nodes).length > 0) {
      Object.keys(nodes).forEach((key) => {
        const location = {
          begin: nodes[key].begin,
          end: nodes[key].end,
          type: locationType.range,
        };
        promises.push(
          createCodeDefect(
            extraInfo.projectId,
            extraInfo.versionId,
            extraInfo.fileId,
            extraInfo.functionId,
            {
              manual: false,
              locations: location,
              type: defectKind.deadEnd,
            }
          )
        );
      });
    }
    return Promise.all(promises);
  });
}
async function getOverview(versionId, query) {
  const ret = {
    pagination: {
      total: 0,
      page: parseInt(query.page, 10) || 1,
      perPage: parseInt(query.perPage, 10) || 20,
    },
    defects: [],
    overview: {
      fileCount: 0,
      functionCount: 0,
      codeDefectCount: 0,
    },
  };
  const filter = { versionId };
  if (query.manual) {
    filter.manual = {
      $in: utility.isArrayType(query.manual) ? query.manual : [].concat(query.manual),
    };
  }
  const defects = await CodeDefect.find(filter, ['fileId', 'functionId']);
  ret.pagination.total = defects.length;
  ret.overview.codeDefectCount = defects.length;
  const defectFileSet = new Set();
  const defectFunctionSet = new Set();
  defects.forEach(({ fileId, functionId }) => {
    fileId = fileId.toString();
    functionId = functionId.toString();
    if (!defectFileSet.has(fileId)) {
      ret.overview.fileCount += 1;
      defectFileSet.add(fileId);
    }
    if (!defectFunctionSet.has(functionId)) {
      ret.overview.functionCount += 1;
      defectFunctionSet.add(functionId);
    }
  });
  const fileMap = new Map();
  const functionMap = new Map();
  (await File.find({ versionId, kind: filesModels.kind.file }, ['_id', 'fileName']).lean()).forEach((file) => {
    fileMap.set(file._id.toString(), file.fileName);
  });
  (await Function.find({ versionId, kind: { $in: fileElementsModels.getCountableKinds() } }, ['_id', 'functionName']).lean()).forEach((func) => {
    functionMap.set(func._id.toString(), func.functionName);
  });
  ret.defects = (await CodeDefect.find(filter)
    .sort({ _id: 1 })
    .skip((ret.pagination.page - 1) * ret.pagination.perPage)
    .limit(ret.pagination.perPage)
    .lean())
    .map((defect, index) => {
      defect.number = (ret.pagination.page - 1) * ret.pagination.perPage + index + 1;
      defect.fileName = fileMap.get(defect.fileId.toString());
      defect.functionName = functionMap.get(defect.functionId.toString());
      return defect;
    });
  return ret;
}
async function exportCodeDefectsOverview(versionId, excelName, locale, query) {
  const filter = { versionId };
  if (query.manual) {
    filter.manual = {
      $in: utility.isArrayType(query.manual) ? query.manual : [].concat(query.manual),
    };
  }
  const fileMap = new Map();
  const functionMap = new Map();
  (await File.find({ versionId, kind: filesModels.kind.file }, ['_id', 'fileName']).lean()).forEach((file) => {
    fileMap.set(file._id.toString(), file.fileName);
  });
  (await Function.find({ versionId, kind: { $in: fileElementsModels.getCountableKinds() } }, ['_id', 'functionName']).lean()).forEach((func) => {
    functionMap.set(func._id.toString(), func.functionName);
  });
  const defects = await CodeDefect.find(filter).lean();
  const cacheDir = path.join(cacheUtil.getConfigPath('exportDir'), randomstring.generate(16));
  await fs.ensureDir(cacheDir);
  const fileExcel = new Excel();
  fileExcel.addSheet('Sheet1');
  const columns = [
    {
      header: locale.__('overview.file.fileName'),
      key: 'fileName',
      width: 16.93,
    },
    {
      header: locale.__('overview.function.functionName'),
      key: 'functionName',
      width: 16.93,
    },
    {
      header: locale.__('overview.codeDefect.description'),
      key: 'description',
      width: 16.93,
    },
    {
      header: locale.__('overview.codeDefect.manual'),
      key: 'manual',
      width: 16.93,
    },
    {
      header: locale.__('overview.codeDefect.readState'),
      key: 'readState',
      width: 16.93,
    },
    {
      header: locale.__('overview.codeDefect.location'),
      key: 'locations',
      width: 16.93,
    },
    {
      header: locale.__('overview.codeDefect.testcases'),
      key: 'testcases',
      width: 16.93,
    },
  ];
  const data = [];
  const defectKindMap = new Map();
  Object.keys(defectKind).forEach((kindName) => {
    defectKindMap.set(defectKind[kindName], kindName);
  });
  defects.forEach((defect) => {
    let begin;
    let end;
    if (defect.locations && defect.locations.length > 0) {
      const location = defect.locations[0];
      begin = location.begin;
      end = location.end;
    }
    const namePrefix = locale.__('overview.test.namePrefix');
    const defectTypeKindName = defectKindMap.get(defect.type);
    data.push({
      description: defect.description || (defectTypeKindName ? locale.__(`overview.codeDefect.${defectTypeKindName}`) : ''),
      manual: `${defect.manual}`.toUpperCase(),
      readState: `${!defect.unread}`.toUpperCase(),
      locations: `${begin ? `(${begin.line},${begin.column})` : ''}${end ? `-(${end.line},${end.column})` : ''}`,
      testcases: defect.testcases.length > 0 ? (defect.testcases.map((testcase) => `${namePrefix}${testcase.number}`)).toString() : '',
      fileName: fileMap.get(defect.fileId.toString()),
      functionName: functionMap.get(defect.functionId.toString()),
    });
  });
  fileExcel.addRows(columns, data);
  // style
  let i = 'A'.charCodeAt(0);
  const clen = i + columns.length;
  for (i; i < clen; i += 1) {
    const len = data.length + 1;
    for (let j = 1; j <= len; j += 1) {
      if (j === 1) {
        fileExcel.setCell(`${String.fromCharCode(i)}${j}`, { font: { bold: true } });
      } else {
        fileExcel.setCell(`${String.fromCharCode(i)}${j}`);
      }
    }
  }
  const outPath = path.join(cacheDir, excelName);
  await fileExcel.writeFile(outPath);
  return [outPath, cacheDir];
}
eventHub.on([eventKind.EVENT_FUNCTION_REMOVE, eventKind.EVENT_FUNCTION_RESET], ({ functionIds, versionId }) => {
  const filter = { versionId: mongoose.Types.ObjectId(versionId) };
  if (!functionIds) {
    return deleteCodeDefects(filter);
  }
  return utility.arrayChunkOperation(utility.arrayChunk(functionIds, 500), 1, (funcIds) => {
    filter.functionId = { $in: funcIds.map((id) => mongoose.Types.ObjectId(id)) };
    return deleteCodeDefects(filter);
  });
});

module.exports = {
  checkDeadEnd,
  createCodeDefect,
  exportCodeDefectsOverview,

  deleteCodeDefect,
  deleteCodeDefects,
  defectKind,
  defectKindDesc,

  getJson,
  getCodeDefect,
  getCodeDefects,
  getCodeDefectsByFildId,
  getCodeDefectsGroup,
  getUnreadTotal,
  getOverview,

  processAutoDefects,

  setCodeDefectToManual,

  updateCodeDefect,
  updateCodeDefects,
};
exports = Object.assign(exports, module.exports);
